// Tierless
// Copyright © 2010 Alphora
// This file is licensed under a modified BSD-license (see tierless_license.txt).

using System;
using System.Linq;

using Tierless.Infrastructure.Data;
using Tierless.Framework.Data;
using Tierless.Framework.Entities;
using Tierless.Framework.EntityTypes;
using Tierless.Framework.Constraints;
using Tierless.Framework.Utilities;
using Tierless.Framework.Server.Data;

namespace Tierless.Framework.Server.Constraints
{
	/*
	 * Enforcement of Temporal references:
	 * 
	 *	There are four possible cases for references:
	 *	
	 *		1) Non-Temporal entity referencing a Non-Temporal entity
	 *		2) Non-Temporal entity referencing a Temporal entity
	 *		3) Temporal entity referencing a Non-Temporal entity
	 *		4) Temporal entity referencing a Temporal entity
	 *		
	 *	Case 1, NT -> NT:
	 *		Non-temporal referencing non-temporal is obviously just a standard reference.
	 *		general constraint ->
	 *			not exists (S { k } without (T { k }))
	 *		source insert case ->
	 *			exists (T where k = @k)
	 *		source update case ->
	 *			@oldk = @k or exists (T where k = @k)
	 *		target update case ->
	 *			@oldk = @k or not exists (S where k = @oldk)
	 *		target delete case ->
	 *			not exists (S where k = @k)
	 *			
	 *	Case 2, NT -> T:
	 *		same as Case 1, if a Non-Temporal record is referencing a Temporal entity, the temporal component is ignored
	 *	Case 3, T -> NT:
	 *		same as Case 1, if a Temporal record references the Non-Temporal entity at any time, the Non-Temporal entity must exist
	 *	Case 4, T -> T:
	 *		general constraint ->
	 *			not exists ((S { k } using { i }) without (T { k } using { i }))
	 *			S { k } using { i } ::=
	 *				select 
	 *						A.k, 
	 *						A.i.From, 
	 *						(
	 *							select top 1 C.i.To
	 *								from S C
	 *									left join S D on C.k = D.k and C.i.To = D.i.From - 1
	 *								where C.k = A.k
	 *									and C.i.From >= A.i.From
	 *									and D.k is null
	 *								order by C.i.From
	 *						) i.To
	 *					from S A
	 *						left join S B on A.k = B.k and A.i.From = B.i.To + 1
	 *					where B.k is null
	 *				
	 *		source insert case ->
	 *			exists (T where k = @k { k } using { i } where T.i.From <= @i.From and (T.i.To is null or (@i.To is not null and T.i.To >= @i.To)))
	 *		source update case ->
	 *			(@oldk = @k and @oldi = @i) or (source insert case)
	 *			
	 *	NOTE: The update & delete cases can not be enforced by looking at the current record only unless the assumption is made
	 *	that an update can never change the timeframe of an entity. Under that assumption, (and given that keys are immutable),
	 *	only the deletion needs to be enforced, and all that needs to be asserted is that there are no referencing entities with a
	 *	timeframe that overlaps the deletion timeframe.
	 * 
	 *		target update case ->
	 *			(@oldk = @k and @oldi = @i) or true
	 *		target delete case ->
	 *			not exists (S where k = @k and S.i.From <= IsNull(@i.To, S.i.From) and @i.From <= IsNull(S.i.To, @i.From))
	 *			
	 */

	/// <summary>
	/// Provides for the enforcement of the source side of a temporal reference.
	/// </summary>
	/// <remarks>
	/// The source side of a temporal reference constraint is enforced similar to the source side of a
	/// non-temporal reference constraint, with the added dimension of time. This means that the targeted
	/// entity must exist throughout the timeframe of the referencing entity. Specifically, the targeted entity
	/// must exist on or before the start of the timeframe of the referencing entity, and must continue to exist
	/// on or after the end (if any) of the timeframe of the referencing entity.
	/// </remarks>
	public class SourceTemporalReference : EntityConstraint
	{
		public SourceTemporalReference
		(
			EntityReference reference,
			ServerEntityType sourceServerEntityType,
			ServerEntityType targetServerEntityType
		)
			: base
			(
				String.Format("{0}{1}_{2}{3}SourceTemporalReference", sourceServerEntityType.Name, reference.SourceColumns.Select(c => c.Name).Combine(), targetServerEntityType.Name, reference.TargetColumns.Select(c => c.Name).Combine()),
				sourceServerEntityType.Name,
				reference.SourceColumns.Select(c => c.Name).Union(new[] { sourceServerEntityType.FromDateColumnName, sourceServerEntityType.ToDateColumnName }).ToArray(),
				ViolationSeverity.Error,
				String.Format("The selected {0} does not exist throughout the timeframe it is being referenced.", targetServerEntityType.Name),
				ConstraintLocation.Server,
				ConstraintTransition.InsertUpdate
			)
		{
			_reference = reference;
			_sourceServerEntityType = sourceServerEntityType;
			_targetServerEntityType = targetServerEntityType;
			BuildStatement();
		}
		
		private EntityReference _reference;
		private ServerEntityType _sourceServerEntityType;
		private ServerEntityType _targetServerEntityType;
		
		private string _statement;
		
		private void BuildStatement()
		{
			// exists (T where k = @k { k } using { i } where T.i.From <= @i.From and (T.i.To is null or (@i.To is not null and T.i.To >= @i.To)))
			
			var conditionString = _reference.TargetColumns.Select(c => "{0}" + c.Name + " = {1}" + c.Name).Combine(" and ");
			
			_statement =
				String.Format
				(
					@"
						select 1
							from
							(
								select
										{1},
										A.{8},
										(
											select top 1 C.{9}
												from {0} C
													left join {0} D on {2} and C.{9} = D.{8} - 1
												where {3}
													and C.{8} >= A.{8}
													and {4}
												order by C.{8}
										) {9}
									from {0} A
										left join {0} B on {5} and A.{8} = B.{9} + 1
									where {6}
							) T
							where {7}
								and T.{8} <= @{8} 
								and (T.{9} is null or (@{9} is not null and T.{9} >= @{9}))
					",
					EntityTypeProvider.GetEntityFullName(_targetServerEntityType),
					_reference.TargetColumns.Select(c => "A." + c.Name).Combine(", "),
					String.Format(conditionString, "C.", "D."),
					String.Format(conditionString, "C.", "A."),
					_reference.TargetColumns.Select(c => "D." + c.Name + " is null").Combine(" and "),
					String.Format(conditionString, "A.", "B."),
					_reference.TargetColumns.Select(c => "B." + c.Name + " is null").Combine(" and "),
					String.Format(conditionString, "T.", "@"),
					_targetServerEntityType.FromDateColumnName,
					_targetServerEntityType.ToDateColumnName
				);
		}

		protected override bool ShouldValidate(ConstraintContext context)
		{
			if (base.ShouldValidate(context))
			{
				if (context.Transition == ConstraintTransition.Update)
				{
					if (_reference.SourceColumns.Any(AColumn => !Equals(context.Entity[AColumn.Name].OriginalValue, context.Entity[AColumn.Name].Value)))
						return true;
							
					if (!Equals(context.Entity.FromDateColumn.OriginalValue, context.Entity.FromDateColumn.Value))
						return true;
						
					return !Equals(context.Entity.ToDateColumn.OriginalValue, context.Entity.ToDateColumn.Value);
				}
				
				return true;
			}
			
			return false;
		}

		protected override bool IsValid(ConstraintContext context)
		{
			var targetEntity = context.FindEntity(_targetServerEntityType.Name, new EntityID(_reference.TargetColumnNames, _reference.SourceColumns.Select((AItem, AIndex) => context.Entity[AItem.Name].Value).ToArray()));
			if (targetEntity != null)
			{
				// TODO: This needs to be expanded to ensure that the targeted timeframe wholly encompasses the source timeframe
				return true;
			}

			var paramsValue = _reference.SourceColumns.Select((AItem, AIndex) => new Param(_reference.TargetColumns[AIndex].Name, context.Entity[AItem.Name].Value)).ToList();
			
			if (paramsValue.Any(p => p.Value == null))
				return true;

			paramsValue.Add(new Param(_targetServerEntityType.FromDateColumnName, context.Entity.FromDateColumn.Value));
			paramsValue.Add(new Param(_targetServerEntityType.ToDateColumnName, context.Entity.ToDateColumn.Value));
			
			using (var reader = ((IServerEntityAccessor)context.EntityAccessor).Database.Open(_statement, paramsValue.ToArray()))
			{
				return (reader.Read());
			}
		}

		protected override ConstraintViolation GetViolation(ConstraintContext context)
		{
			return 
				new ConstraintViolation
				(
					Name, 
					EntityTypeName, 
					context.ColumnNames, 
					ColumnNames, 
					context.Severity, 
					String.Format
					(
						"There is no {0} with the selected ID ({1}) in the range {2} to {3}.", 
						_targetServerEntityType.Name, 
						new EntityID(context.Entity, _reference.SourceColumns.Select(c => c.Name).ToArray()),
						context.Entity.FromDate.ToShortDateString(),
						context.Entity.ToDate.HasValue ? context.Entity.ToDate.Value.ToShortDateString() : "<ongoing>"
					)
				);
		}
	}
}
